
#include "Common.hpp"
using namespace std;

namespace {

//-----------------------------------------------------------------------------
const char* nVertexProgramSrc =
    "!!NVvp4.0"
    NL "FLOAT ATTRIB iPosition = vertex.attrib[0];"
    NL "FLOAT ATTRIB iColor = vertex.attrib[1];"
    NL "FLOAT PARAM ModelviewProj[4] = { program.local[0..3] };"
    NL "FLOAT OUTPUT oPosition = result.position;"
    NL "FLOAT OUTPUT oNewPosition = result.attrib[0];"
    NL "FLOAT OUTPUT oColor = result.attrib[1];"
    NL
    NL "main:"
    NL "    DP4.F oPosition.x, ModelviewProj[0], iPosition;"
    NL "    DP4.F oPosition.y, ModelviewProj[1], iPosition;"
    NL "    DP4.F oPosition.z, ModelviewProj[2], iPosition;"
    NL "    DP4.F oPosition.w, ModelviewProj[3], iPosition;"
    NL
    NL "    MUL oNewPosition.xy, iPosition, 0.8;"
    NL "    ADD oNewPosition.z, iPosition, 0.1;"
    NL "    MOV oColor, iColor;"
    NL "    RET;"
    NL "END";

const char* nFragmentProgramSrc =
    "!!NVfp4.0"
    NL "FLOAT ATTRIB iColor = fragment.attrib[1];"
    NL "FLOAT OUTPUT oColor = result.color;"
    NL
    NL "main:"
    NL "    MOV oColor, iColor;"
    NL "    RET;"
    NL "END";
//-----------------------------------------------------------------------------
Window* nWindow;
SDL_GLContext nContext;

GLuint nProgram[2];
GLuint nXFO[2];
GLuint nBuffer[3];
GLuint64 nBufferAddress[2];

ArcBall nArcBall;
//-----------------------------------------------------------------------------
void Setup() {
    // window
    nWindow = new Window("Transform Feedback", 800, 600);

    // OpenGL context
    nContext = SDL_GL_CreateContext(nWindow->Get());
    if (!nContext) {
        throw runtime_error("[Setup] SDL_GL_CreateContext failed.");
    }

    SDL_GL_SetSwapInterval(0);

    if (glewInit() != GLEW_OK) {
        throw runtime_error("[Setup] glewInit failed.");
    }
    if (!GLEW_NV_gpu_program4) {
        throw runtime_error("[Setup] GL_NV_gpu_program4 extension is not supported.");
    }

    // programs
    glGenProgramsARB(2, nProgram);

    glBindProgramARB(GL_VERTEX_PROGRAM_ARB, nProgram[0]);
    LoadGLProgram(GL_VERTEX_PROGRAM_ARB, nVertexProgramSrc);
    cout << GetGLProgramInfo(GL_VERTEX_PROGRAM_ARB);

    glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, nProgram[1]);
    LoadGLProgram(GL_FRAGMENT_PROGRAM_ARB, nFragmentProgramSrc);
    cout << GetGLProgramInfo(GL_FRAGMENT_PROGRAM_ARB);

    // buffers
    const float s = 1.0f;
    const float vertices[] = {
        -s, -s, 0.0f, 1.0f, 0.0f, 0.0f,
        s, -s, 0.0f, 0.0f, 1.0f, 0.0f,
        0.0f, s, 0.0f, 0.0f, 0.0f, 1.0f
    };

    glGenBuffers(3, nBuffer);

    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, nBuffer[0]);
    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, sizeof(vertices), NULL, GL_STATIC_DRAW);
    glGetBufferParameterui64vNV(GL_TRANSFORM_FEEDBACK_BUFFER, GL_BUFFER_GPU_ADDRESS_NV, &nBufferAddress[0]);
    glMakeBufferResidentNV(GL_TRANSFORM_FEEDBACK_BUFFER, GL_READ_ONLY);

    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, nBuffer[1]);
    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, sizeof(vertices), NULL, GL_STATIC_DRAW);
    glGetBufferParameterui64vNV(GL_TRANSFORM_FEEDBACK_BUFFER, GL_BUFFER_GPU_ADDRESS_NV, &nBufferAddress[1]);
    glMakeBufferResidentNV(GL_TRANSFORM_FEEDBACK_BUFFER, GL_READ_ONLY);

    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, nBuffer[2]);
    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // XFOs
    const GLint attribs[] = {
        GL_GENERIC_ATTRIB_NV, 3, 0,
        GL_GENERIC_ATTRIB_NV, 3, 1,
    };

    glGenTransformFeedbacks(2, nXFO);

    glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, nXFO[0]);
    glTransformFeedbackAttribsNV(2, attribs, GL_INTERLEAVED_ATTRIBS);
    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, nBuffer[1]);

    glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, nXFO[1]);
    glTransformFeedbackAttribsNV(2, attribs, GL_INTERLEAVED_ATTRIBS);
    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, nBuffer[0]);

    // GL state
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_VERTEX_PROGRAM_ARB);
    glEnable(GL_FRAGMENT_PROGRAM_ARB);

    // GUI
    TwInit(TW_OPENGL, NULL);
    TwWindowSize(nWindow->Width(), nWindow->Height());

    TwBar* bar = TwNewBar("Usage");
    TwDefine("Usage color='0 0 0' alpha=128 position='10 10' size='240 50'");
    TwDefine("Usage fontresizable=false resizable=false movable=false");
    TwAddButton(bar, "usage0", NULL, NULL, "label='- mouse with LB down to rotate'");

    nArcBall.SetWindowSize(nWindow->Width(), nWindow->Height());
}
//-----------------------------------------------------------------------------
bool ProcessEvents() {
    SDL_Event evt;
    while (SDL_PollEvent(&evt)) {
        if (evt.type == SDL_QUIT) {
            return false;
        }

        if (ProcessCommonEvent(evt) || ProcessGUIEvent(evt)) {
            continue;
        }

        switch (evt.type) {
        case SDL_MOUSEBUTTONDOWN:
            if (evt.button.button == SDL_BUTTON_LEFT) {
                nArcBall.Begin(evt.button.x, evt.button.y);
                return true;
            }
            break;

        case SDL_MOUSEBUTTONUP:
            if (evt.button.button == SDL_BUTTON_LEFT) {
                nArcBall.End();
                return true;
            }
            break;

        case SDL_MOUSEMOTION:
            nArcBall.Drag(evt.motion.x, evt.motion.y);
            break;
        }
    }

    return true;
}
//-----------------------------------------------------------------------------
void Update() {
    // clear buffers
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // compute transform matrices
    const glm::mat4 m = nArcBall.Rotation();
    const glm::mat4 mv = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f)) * m;
    const glm::mat4 mvp = glm::transpose(glm::perspective(45.0f, 1.333f, 0.1f, 10.0f) * mv);

    glBindProgramARB(GL_VERTEX_PROGRAM_ARB, nProgram[0]);
    glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, nProgram[1]);

    glProgramLocalParameters4fvEXT(GL_VERTEX_PROGRAM_ARB, 0, 4, &mvp[0][0]);

    glBindBuffer(GL_ARRAY_BUFFER, nBuffer[0]);
    glBindBuffer(GL_COPY_READ_BUFFER, nBuffer[2]);
    glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_ARRAY_BUFFER, 0, 0, 18 * sizeof(float));

    glEnableClientState(GL_VERTEX_ATTRIB_ARRAY_UNIFIED_NV);
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glVertexAttribFormatNV(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float));
    glVertexAttribFormatNV(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float));

    const GLsizeiptr size = 3 * 6 * sizeof(float);
    glBufferAddressRangeNV(GL_VERTEX_ATTRIB_ARRAY_ADDRESS_NV, 0, nBufferAddress[0], size);
    glBufferAddressRangeNV(GL_VERTEX_ATTRIB_ARRAY_ADDRESS_NV, 1, nBufferAddress[0] + sizeof(float) * 3, size - sizeof(float) * 3);

    glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, nXFO[0]);

    glBeginTransformFeedback(GL_TRIANGLES);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glEndTransformFeedback();

    for (int i = 0; i < 8; ++i) {
        const int idx = (i + 1) % 2;

        glBufferAddressRangeNV(GL_VERTEX_ATTRIB_ARRAY_ADDRESS_NV, 0, nBufferAddress[idx], size);
        glBufferAddressRangeNV(GL_VERTEX_ATTRIB_ARRAY_ADDRESS_NV, 1, nBufferAddress[idx] + sizeof(float) * 3, size - sizeof(float) * 3);

        glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, nXFO[idx]);

        glBeginTransformFeedback(GL_TRIANGLES);
        glDrawTransformFeedback(GL_TRIANGLES, nXFO[0]);
        glEndTransformFeedback();
    }

    // draw GUI
    glDisableClientState(GL_VERTEX_ATTRIB_ARRAY_UNIFIED_NV);
    glDisableClientState(GL_ELEMENT_ARRAY_UNIFIED_NV);
    for (GLuint i = 0; i < 16; ++i) {
        glDisableVertexAttribArray(i);
    }
    TwDraw();

    nWindow->Swap();
}
//-----------------------------------------------------------------------------
void Run() {
    Setup();

    while (ProcessEvents()) {
        Update();
    }
}
//-----------------------------------------------------------------------------
void Cleanup() {
    glDeleteBuffers(3, nBuffer);
    glDeleteTransformFeedbacks(2, nXFO);
    glDeleteProgramsARB(2, nProgram);

    TwTerminate();

    SDL_GL_DeleteContext(nContext);
    delete nWindow;
}
//-----------------------------------------------------------------------------

} // namespace

//-----------------------------------------------------------------------------
int main(int argc, char** argv) {
    if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO) < 0) {
        cout << "[main] SDL_Init failed." << endl;
        return 0;
    }

    try {
        Run();
    }
    catch (const exception& ex) {
        cout << ex.what() << endl;
    }
    catch (...) {
        cout << "Undefined exception." << endl;
    }

    Cleanup();
    SDL_Quit();

    return 0;
}
//-----------------------------------------------------------------------------
